Skip to content

fix(spec): $between endpoints accept the ISO/clock strings the platform produces (#6571) - #7058

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6571-between-string-endpoints
Aug 9, 2026
Merged

fix(spec): $between endpoints accept the ISO/clock strings the platform produces (#6571)#7058
os-zhuang merged 1 commit into
mainfrom
claude/issue-6571-between-string-endpoints

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #6571

The sibling half of #5685 (PR #6570). Both of $between's endpoints declared
number | Date | FieldReference — and the platform's own producers put a
string in them. As with the four ordering slots, the declaration did not
merely under-describe reality, it contradicted it; and it did so in the one slot
where a closed interval is the natural spelling of a date window, which is
what makes this the shape an author — an AI author in particular — is most
likely to reach for.

Premise, re-verified against origin/main (not the issue's line numbers)

The issue was filed 2026-08-08 and filter.zod.ts took a docs-prose merge since,
so every claim was re-measured in this worktree at 08863dd18:

claim measured
RangeOperatorSchema.$between endpoints lack string confirmed, filter.zod.ts:232-238
FieldOperatorsSchema.$between endpoints lack string confirmed, filter.zod.ts:422-425
Filter< T > guard is T[K] extends number ǀ Date ? [T[K], T[K]] : never confirmed, filter.zod.ts:630
the date-macro walker descends into arrays confirmed, packages/core/src/utils/filter-tokens.ts:386

Premise valid on all four points.

Evidence the string endpoint is the platform's own output

  1. The date-macro resolver descends into arrays. resolveFilterTokens
    (@objectstack/core, filter-tokens.ts) evaluates the {token} grammar and
    its walk carries an explicit array arm, so a tuple comparand is resolved
    member by member. Every branch of that resolver returns a string — asYmd(…)
    for a calendar day, .toISOString() for the sub-day tokens. So
    { close_date: { $between: ['{current_year_start}', '{current_year_end}'] } }
    becomes { close_date: { $between: ['2026-01-01', '2026-12-31'] } }, whose
    two endpoints were exactly the type this schema declared it refused.
  2. This package's own conformance corpus already spells it — a stronger
    witness than ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685 had. packages/spec/src/data/temporal-conformance.ts, the
    shared cross-driver expectation table, states three $between cases with
    string endpoints: a datetime range with its {90_days_ago}/{today} token
    twin (:294), the degenerate single-day range (:303), and
    { at: { $between: ['08:00:00', '18:00:00'] } } on a Field.time column
    (:537). A declaration contradicted by the conformance table in the same
    package is not under-describing reality; it is disagreeing with it.
  3. The driver already normalises both ends per column type.
    SqlDriver.coerceFilterValue recurses through arrays member-wise
    (sql-driver.ts:7333, value.map(v => this.coerceFilterValue(table, field, v))),
    and calendarDayBetweenRewrite (:7413) coerces the min and rewrites a
    bare-calendar-day max into the half-open < next-day(max) bound — knex's
    whereBetween being inclusive on both ends, it inherits the same rule $lte
    has (dashboard 的日期区间上界打在 datetime 列上丢失当天数据 —— 默认配置即命中 #3777).

What changed — three spellings, per the ruling

The triage promotion of 2026-08-08 settles the direction, verbatim and
untranslated:

the #5685 precedent settles the fix direction. Fix both spellings plus the Filter guard; #6570's docblock is the template.

  • RangeOperatorSchema — the documentation copy. z.string() joins both
    endpoint unions, plus a docblock on fix(spec): $gt/$gte/$lt/$lte 接受平台自己产出的 ISO 字符串 (#5685) #6570's template and a
    RANGE_ENDPOINT_DESCRIPTION .describe() (module-private, mirroring
    ORDERING_COMPARAND_DESCRIPTION, so it stays off the exported API surface).
  • FieldOperatorsSchema.$between — the enforced copy, the one
    NormalizedFilterSchema validates against and FieldOperators is inferred
    from. ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685 moved the documentation copy first and had to come back for the
    reachable one; both move together here.
  • Filter< T > — the typed half, mirroring the ordering guard slot for slot.
    T is known here, so it stays type-precise rather than admitting string
    everywhere: a Date field also takes the resolver's ISO strings, a string
    field (a Field.time '08:00:00', an autonumber code) becomes rangeable
    instead of collapsing to never, and a number field stays numbers-only.
    Each endpoint widens independently, so a partially-resolved range
    ([Date, '2026-12-31']) type-checks — which is what a partial macro
    resolution actually hands the author.

Why a bare string and not an ISO refinement

Same finding as #6570, re-measured for the tuple: this schema is
field-agnostic (it never sees which column the range applies to); an ISO
refinement would reject the HH:MM[:SS[.fff]] form field-value.zod.ts's
CLOCK_TIME_TYPES declares NOT Date.parse-able and which the conformance case
above exercises; and date-only vs full-timestamp is already reconciled
downstream by calendarDayBetweenRewrite. Endpoint-vs-column correctness is a
field-TYPED judgement that already has an owner.

Stated plainly in the docblock: ranging over non-temporal text is permitted
but not promised (the order is the backend collation's), and nothing promises
the two endpoints are ordered relative to each other — an inverted [max, min]
range is well-formed and matches nothing, at every backend.

Why the filter-semantics compile faces need zero changes

The five compile faces of the filter-semantics surface (driver-sql, driver-memory,
driver-turso, the analytics normaliser, the formula matcher) are deliberately
untouched, and that is the correct outcome
: this is a declaration-side widening
of a type that no compile face consults. The endpoints were already being
normalised driver-side by column type before ever reaching a predicate —
coerceFilterValue maps arrays member-wise and calendarDayBetweenRewrite owns
the calendar-day max — so every filter that validated before still validates and
every filter that compiled before still compiles the same SQL. Re-measured in
this worktree; the sentence still holds.

Scope was held to $between exactly. #6520 ($icontains) is a different
operator and is explicitly out of this card.

Tests, both directions

packages/spec/src/data/filter.test.ts:

  • now accepted: the two calendar-day strings a token range resolves to; a
    range of full ISO instants; the ['08:00:00', '18:00:00'] wall-clock range
    the conformance corpus pins; mixed endpoints (Date + string, string +
    { $field }) proving each union resolves on its own.
  • still accepted (additive): numbers, Dates, { $field } references.
  • still rejected: an endpoint rangeable at no backend (true, a bare
    object), and non-two-element arity.
  • enforced copy pinned: the same shapes through FieldOperatorsSchema and
    NormalizedFilterSchema, so the two spellings cannot drift.
  • typed half: a Filter< Deal > block checked by tsc, not vitest.

The rejection cases assert through safeParse and check the failing issue's
path (['$between', 0] vs ['$between', 1]) rather than a bare toThrow():
a tuple carries two independent unions, and toThrow() cannot tell a min-side
refusal from a max-side one. The ADR-0112 code/status envelope does not
apply — these are Zod parse verdicts on a declaration surface, not runtime
refusals.

Reverse verification (direction predicted first)

Predicted before running: reverting the union widening turns the new
string-endpoint acceptance pins RED, and the Filter< T > block goes red under
tsc only — vitest never type-checks, so the typed pin is green under vitest on
the reverted schema by construction. Result recorded in the dev report.

Gates

node scripts/check-adr-0087-registration.mjs --base origin/main
this PR adds no declared-breaking changeset (additive widening; no D2/D3
registration owed). pnpm --filter @objectstack/spec check:generated → all 10
generated artifacts up to date, nothing regenerated and no generated tree
touched (the operator .describe() strings do not reach
content/docs/references/, same as #6570's). scripts/check-nul-bytes.mjs OK.

Changeset graded minor on @objectstack/spec, matching #6570's own changeset
for the same class of widening.


Generated by Claude Code

…rm produces (#6571)

The sibling half of #5685. Both of `$between`'s endpoints declared
`number | Date | FieldReference`, while the platform's own producers put a
string in them:

- `resolveFilterTokens` (`@objectstack/core`) walks INTO arrays, so a token
  range resolves member by member and every resolver branch returns a string:
  `['{current_year_start}', '{current_year_end}']` -> `['2026-01-01', '2026-12-31']`.
- This package's own `temporal-conformance.ts` corpus states three `$between`
  cases with string endpoints, including `['08:00:00', '18:00:00']` on a
  `Field.time` column.
- `SqlDriver.coerceFilterValue` already normalises arrays member-wise, and
  `calendarDayBetweenRewrite` coerces the min and rewrites a bare-calendar-day
  max into the half-open next-day bound (#3777).

Widened in all three places the contract is spelled — `RangeOperatorSchema`
(documentation), `FieldOperatorsSchema` (the enforced copy behind
`NormalizedFilterSchema`), and the `Filter<T>` helper, where `T` is known so the
guard stays type-precise and each endpoint widens independently.

Declaration-side and additive: no producer, caller or driver changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 12:32pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding][spec] $between 的两个端点也不含 string —— 与 #5685 同一处矛盾,而日期宏解析器会走进数组

2 participants